# coding=utf-8
# 本文部分参数设置参考 https://www.joinquant.com/view/community/detail/5688e33ac78e7df98d9cf55c494e02b2?type=1
import backtrader as bt
from DB.FuturesDB import getFutureMinPrice, getFutureDailyPrice
from DB.OthersDB import getTradeDays, getTradeDaysWithCount, getAllTradeDays
from datetime import date, timedelta, datetime
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import talib as tlb

#正常显示画图时出现的中文和负号
from pylab import mpl
mpl.rcParams['font.sans-serif']=['SimHei']
mpl.rcParams['axes.unicode_minus']=False


# 获取数据的频率跟策略执行的一致
def build_backtest_dataset(start_date: str, end_date: str) -> pd.DataFrame:
    """构造配套 backtesting 用的数据集"""
    # Backtest 的数据源为一个包含 4 列 ['Open', 'High', 'Low', 'Close'] 的 dataframe
    df = getFutureMinPrice(start_date, end_date, 'T9999_Min', format=1)
    df = df[[ 'open', 'high', 'low', 'close', 'volume']]
    df = df.sort_index()
    df['openinterest'] = 0

    return df


def get_daily_data(start_date:str, end_date:str):
    df = getFutureDailyPrice(start_date, end_date, 'T9999_Daily')
    return df


class DTWeekly(bt.Strategy):
    # 作者
    author = 'zhujian'
    # 待优化参数 -上轨，下轨，往前取的交易日天数
    # params = (('buy_ratio', 1.5), ('sell_ratio', 4), ('N', 20), )
    params = dict(calcRange=[1], )

    def log(self, txt, dt=None):
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))         # 打印日期和收盘价格


    def __init__(self):
        # 初始化交易指令、买卖价格和手续费
        self.inds = dict()
        self.trades = dict()
        self.orders = dict()
        self.order = None
        self.buyprice = None
        self.buycomm = None
        self.long_Ratio = 0.15
        self.short_Ratio = 0.4
        self.N = 20 #往前取的交易日数，用于上下轨计算
        self.N2 = 125 # 往前取的交易日数，用于机器学习训练集
        self.dtrange = 0  # 轨宽
        self.buyline = 0  # 上轨
        self.sellline = 0  # 下轨
        self.bar_num = 0 #next运行了多少个bar
        self.current_date = self.prev_date = None #当前交易日
        self.day_cross = False #发生跨日标志
        self.weekly_open = 0
        self.symbol = 'T9999.CCFX'
        self.min_data = self.datas[0]
        # self.begin_idx = 1
        # self.contractUnit = 10000
        self.futures_margin_rate = 0.02
        self.risk_expo1 = 2
        self.risk_expo2 = 5
        self.conf_lvl1 = 99
        self.conf_lvl2 = 95
        prev_training_date = getTradeDaysWithCount('2019-01-07', -self.N2-1)[-1]
        self.train_trade = getTradeDays(str(prev_training_date), '2019-12-31').tolist() # 训练集日期
        self.train_d = get_daily_data(str(prev_training_date), '2019-12-31') #日线数据训练集
        # self.pos_limit = 0.3
        self.chgpos_l = self.chgpos_s = -1
        self.var_level = False

        # 中间变量
        self.market_close = False

    #   定时器 - 每周一计算Range，进而进一步得到上下轨
        self.add_timer(when=bt.Timer.SESSION_START,
                       weekdays=self.p.calcRange,
                       weekcarry=True)

    def prenext(self):
    #     由于期货交易日期不同，不会自动进入next，需要在每个prenext中调用
        self.next()

    # day: 检查输入的某个交易日是否当周首个
    def check_weekly_begin(self, day):
        begin = False

        #     当天为周一的直接返回True，否则判断是否为非周一的首个交易日
        if day.isoweekday() == 1:
            begin = True
        else:
            prvdate = day - timedelta(days=1)
            prvdate2 = day - timedelta(days=2)
            firstday_flag = (prvdate not in self.train_trade) and (prvdate2 not in self.train_trade)

            if firstday_flag:
                begin = True

        return begin


    # 计算上下轨并返回
    def get_buysell_lines(self, bar_time, bar_open, train_d):
        print('开始计算轨宽')
        recal_flag = False
        # 1.不为零，直接返回
        weekly_begin = None
        # 2.1判断当前bar是否本周开盘bar——Yes：重新计算range；No：pass
        if bar_time.date().isoweekday()==1: # 2.1.1——周一开盘bar
            weekly_begin = bar_time.date()
            self.weekly_open = bar_open
            print('周一 range重新计算, 本周开盘价：', self.weekly_open)
            # 开盘后才启动程序，取当天bar的open
            recal_flag = True
        else:# 2 非周一
            # print('非周一')
            prvdate = bar_time - timedelta(days=1)
            previous_str = prvdate.strftime('%Y-%m-%d')
            prvdate2 = bar_time - timedelta(days=2)
            previous2_str = prvdate2.strftime('%Y-%m-%d')
            firstday_flag = (previous_str not in self.train_trade) and (previous2_str not in self.train_trade)

            if firstday_flag:# 2.2 若非周一，而且昨天和前天均不是交易日，则今天是本周首个交易日。重新计算range
                weekly_begin = bar_time.date()
                recal_flag = True
                print('今天周', str(weekly_begin.isoweekday()), ' 为本周首个交易日')
            else: # 非首个交易日，但还是要以本周一作为计算起点
                print('往前推算本周首个交易日')
                i=1
                currday = bar_time.date()
                while i<6 and weekly_begin is None:
                    prvdate1 = currday - timedelta(days=i)
                    prvdate1_str = prvdate1.strftime('%Y%m%d')
                    prvdate2 = currday - timedelta(days=i+1)
                    prvdate2_str = prvdate2.strftime('%Y%m%d')

                    if (prvdate1_str not in self.train_trade) and (prvdate2_str not in self.train_trade):
                        weekly_begin = currday - timedelta(days=i-1)
                    i+=1
                    # print('i is ', i, prvdate1_str, prvdate2_str, prvdate1_str not in self.train_trade, prvdate2_str not in self.train_trade)
                recal_flag = True
                print('今天周', str(currday.isoweekday()), ' 本周首个交易日是 ', weekly_begin, type(weekly_begin))

        if self.weekly_open==0:
            tdict = self.train_d.loc[weekly_begin]
            if not tdict:
                print('本周首个交易日行情获取失败')
                return
            else:
                self.weekly_open = tdict['open']

        if recal_flag:
            endx = self.train_trade.index(self.current_date) - 1
            stndx = self.train_trade.index(self.current_date) - (1 + self.N)
            print('stndx=', stndx)
            hist = self.train_d.iloc[stndx:endx]
            self.dtrange = self.Dual_Thrust_Range(hist)
            self.buyline = self.weekly_open + self.dtrange * self.long_Ratio
            self.sellline = self.weekly_open - self.dtrange * self.short_Ratio
            print('开盘价：%0.3f, 轨宽：%0.3f, 上轨: %0.3f, 下轨: %0.3f' % (self.weekly_open, self.dtrange, self.buyline, self.sellline))


    def buy_sell(self):
        if self.weekly_open==0 or self.dtrange==0:
            print('当周开盘价未获取，或上下轨未计算，返回')
            return
        else:
            current_price = self.min_data.close[0]
            curr_pos = self.getpositionbyname(self.min_data._name)

            if current_price>self.buyline and curr_pos.size < 3:
                self.log('开多')
                self.buy()

            if current_price < self.sellline and curr_pos.size > -3:
                self.log('开空')
                self.sell()


    def next(self):
        info = self.broker.getcommissioninfo(self.data)
        # print('broker.get_leverage: {}'.format(self.broker.get_leverage()))
        # print('getcomissioninfo.get_leverage: {}'.format(info.get_leverage()))

        # 合约乘数
        mult = info.p.mult
        # 保证金率
        margin = info.p.margin
        # 假设成交的价格是price，则一手的名义价值为：
        nominal_value = 1 * 100 * mult
        # 1手需要的保证金为：
        require_margin = nominal_value * margin
        # print('合约乘数：', mult, margin, nominal_value, require_margin)

        if self.current_date is not None:
            self.prev_date = self.current_date
        btime = bt.num2date(self.min_data.datetime[0])
        self.current_date = btime.date()
        # 获取当周开盘价及计算上下轨
        if self.dtrange==0:
            self.get_buysell_lines(btime, self.min_data.open[0], self.train_d)
        # 判断是否跨日，或者首日计算
        if self.prev_date != self.current_date or self.prev_date is None:
            self.day_cross = True
            # VaR 仓位控制还没生效，需要计算一次
            self.chgpos_l, self.chgpos_s, self.var_level = self.checkVaR()

        # 出现仓位变化再打印
        if self.getposition(self.data).size !=0:
            print('当前仓位：', self.getposition(self.data).size)

        self.bar_num+=1

        self.buy_sell()

        #将收盘价保留两位小数再输出
        # self.log('Close: %.2f' % self.data.close[0])
        # for i, d in enumerate(self.datas):
        #     print('next:', i, d)
        #     position = self.getposition(d).size
        if self.order is None:  # 检查订单是否挂起。。。如果是，我们无法发送第二个
            # print('暂未收到指令')
            return


    def notify_order(self, order):
        # 如果order为submitted/accepted,返回空
        if order.status in [order.Submitted, order.Accepted]:
            return
        # 如果order为buy/sell executed,报告价格结果
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(f'买入:\n价格:{order.executed.price}, 成本:{order.executed.value}, 手续费:{order.executed.comm}')
                self.buyprice = order.executed.price
                self.buycomm = order.executed.comm
            else:
                self.log(f'卖出:\n价格：{order.executed.price}, 成本: {order.executed.value}, 手续费{order.executed.comm}')
            self.bar_executed = len(self)

            # 如果指令取消/交易失败, 报告结果
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('交易失败')
        self.order = None
        # 记录交易收益情况（可省略，默认不输出结果）
        def notify_trade(self, trade):
            if not trade.isclosed:
                return
            self.log(f'策略收益：\n毛收益 {trade.pnl:.2f}, 净收益 {trade.pnlcomm:.2f}')


    def notify_trade(self, trade):
        if not trade.isclosed:
            return

        self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
                 (trade.pnl, trade.pnlcomm))


    def stop(self):
        pass


    def Dual_Thrust_Range(self, hist):
        HH = max(hist['high'])  # 前N日所有bar的Close最高价
        HC = max(hist['close'])  # 前N日所有bar的Close最高价
        LC = min(hist['close'])  # 前N日所有bar的Close最低价
        LL = min(hist['low'])  # 前N日所有bar的Close最高价

        return round(max((HH - LC), (HC - LL)), 4)

    # 每天运行一次
    def checkVaR(self):
        endx = self.train_trade.index(self.current_date) - 1
        stndx = self.train_trade.index(self.current_date) - self.N2
        if endx<0 or stndx<0:
            print('起止日期无效')
            # return
        print('stndx=', stndx, 'endx=', endx)
        price = self.train_d.iloc[stndx:endx]

        # 这里的长短日期是根据聚宽研究 - 静态调参-国债期货周内的优化结果而定
        ema_s = tlb.EMA(price['close'][-10:], 10)[-1]
        ema_l = tlb.EMA(price['close'][-34:], 34)[-1]
        level = 1 # 看多
        if ema_s<=ema_l:
            level=0

        R = [self.risk_expo1, self.risk_expo2]
        C = [self.conf_lvl1, self.conf_lvl2]

        price_df = pd.DataFrame(price)
        # print('price=',price_df.head(1))
        chage = price_df['close'].pct_change().dropna(axis=0)
        VaR_l = np.nanpercentile(chage, (100 - C[level]))
        VaR_s = np.nanpercentile(chage, (100 - C[abs(level-1)]))
        target_l = -1*R[level]/VaR_l
        target_l = round(target_l*self.futures_margin_rate, 6)
        target_l = min(target_l, 100)
        target_s = -1*R[abs(level-1)]/VaR_s
        target_s = round(target_s * self.futures_margin_rate, 6)
        target_s = min(target_s, 100)
        print('VaR=', VaR_l, VaR_s, ' target pos=', target_l, target_s)
        return target_l, target_s, level

        # 每天收盘后执行的函数
    def after_market_close(self):
        self.day_cross = True  # 跨日flag
        # self.buynum = self.sellnum = 0  # 当天开多开空次数统计清零
        # self.daily_long_close_num = self.daily_short_close_num = self.daily_long_in_num = self.daily_short_in_num = 0  # 上下轨附近止损统计清零


    def testTimer(self):
        print('weekly begin test')

    def notify_timer(self, timer, when, *args, **kwargs):
        self.testTimer()


# 自定义期货百分比费用
class CommInfo_Fut_Perc_Mult(bt.CommInfoBase):
    params = (
      ('stocklike', False), # 指定为期货模式
      ('commtype', bt.CommInfoBase.COMM_PERC), # 使用百分比费用
      ('percabs', True), # commission 以 % 为单位
    )

    def _getcommission(self, size, price, pseudoexec):
        # 计算交易费用
        fee = (abs(size) * price) * (self.p.commission/100) * self.p.mult
        print('fee:', size, price, self.p.commission, self.p.mult)
        return fee
    # pseudoexec 用于提示当前是否在真实统计交易费用
    # 如果只是试算费用，pseudoexec=False
    # 如果是真实的统计费用，pseudoexec=True

if __name__ == '__main__':
    train_start = '2019-01-07'
    train_end = '2019-12-31'
    test_sd = '2020-01-01'
    test_ed = '2020-12-31'

    train_min = build_backtest_dataset(train_start, train_end)
    # min_feed = bt.feeds.PandasData(dataname=train_min, name='min_data',
    #                                      datetime=0, open=1, high=2, low=3, close=4, volume=5)
    min_feed = bt.feeds.PandasData(dataname=train_min)
    # train_d = get_daily_data(train_start, train_end)
    # day_feed = bt.feeds.PandasData(dataname=train_d, name='day_data')
    # print(train_min.head())

    # 初始化Cebro引擎
    cerebro = bt.Cerebro()
    ##给Cebro引擎添加数据
    cerebro.adddata(min_feed)
    # 给Cebro引擎添加策略
    cerebro.addstrategy(DTWeekly)
    # 设置初始资本为100,000
    startcash = 100000
    cerebro.broker.setcash(startcash)
    # 设置期货佣金模式
    # comminfo = CommInfo_Fut_Perc_Mult(commission=0.003,  mult=10000, margin=2)  # 实例化
    # cerebro.broker.addcommissioninfo(comminfo)
    cerebro.broker.setcommission(commission=0.3, automargin=-1, mult=10000, leverage=100,
                                 percabs=True, commtype=bt.CommInfoBase.COMM_PERC, stocklike=False)

    # cerebro.addsizer(bt.sizers.FixedSize, stake=1)

    # 运行Cebro引擎
    cerebro.run(maxcpus=8)
    # 获取回测结束后的总资金
    portvalue = cerebro.broker.getvalue()
    pnl = portvalue - startcash
    # 打印结果
    print(f'总资金: {round(portvalue, 3)}')

    # cerebro.plot(style='candlestick')